.NET: Bug fixes for declarative workflows#6427
Conversation
There was a problem hiding this comment.
Pull request overview
This PR tightens correctness and security of declarative workflow execution, focusing on (1) preventing TOCTOU-style state mutation during approval windows for function-tool invocations, and (2) improving MCP client caching isolation so different auth/header/connection contexts don’t share the same cached client.
Changes:
- Capture and persist an approval-time snapshot for
InvokeFunctionToolExecutor, use it on resume, and clear it after completion. - Update
DefaultMcpToolHandlerclient caching to discriminate by URL/label/connection/headers (with a deterministic SHA-256 header-set hash) and harden defaultHttpClienthandler behavior. - Add targeted unit tests covering approval snapshot behavior and MCP cache-key/hash discrimination.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeFunctionToolExecutor.cs | Adds approval-time parameter snapshotting + checkpoint persistence and uses the snapshot to avoid state-mutation issues during approval. |
| dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeFunctionToolExecutorTest.cs | Adds tests validating snapshot immutability across approval windows and checkpoint/restore, plus cleanup behavior. |
| dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/DefaultMcpToolHandler.cs | Makes MCP client caching key include label/connection/headers, introduces deterministic header hashing, and adjusts default HttpClient handler setup. |
| dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/DefaultMcpToolHandlerTests.cs | Adds unit tests for header hashing and cache-key discrimination dimensions. |
There was a problem hiding this comment.
Automated Code Review
Reviewers: 5 | Confidence: 87%
✓ Correctness
This PR makes two solid bug fixes: (1) the MCP client cache key is expanded from a simple normalized URL string to a 4-tuple including server label, connection name, and a SHA256-based headers hash, fixing isolation bugs when the same URL is used with different credentials/connections; (2) an approval snapshot mechanism prevents state mutation during the approval window from changing which function/arguments are actually invoked. Both changes are well-tested and correctly implemented. The previously-reported issue (passing untrimmed URL to CreateClientAsync) has been addressed. No blocking correctness issues found.
✓ Security Reliability
The PR introduces two solid security improvements: (1) approval snapshot mechanism preventing TOCTOU attacks on function invocation, and (2) proper cache key discrimination for MCP clients. One medium-severity hash collision vulnerability exists in ComputeHeadersHash where the
key:value\nserialization format is ambiguous when header values contain newlines, potentially allowing different credential sets to map to the same cached MCP client.
✓ Test Coverage
The PR adds strong test coverage for the new approval-snapshot security feature and the MCP cache-key improvements. However, there is one new production code path — the backward-compatibility fallback in InvokeRegisteredFunctionAsync when _approvalSnapshot is null — that has no corresponding test.
✓ Failure Modes
The PR fixes two distinct issues: (1) MCP client cache key isolation is improved by using a 4-tuple (URL, label, connection, headers-hash) with SHA256 for deterministic hashing, and cookie isolation via UseCookies=false; (2) a TOCTOU security fix for InvokeFunctionToolExecutor captures an approval snapshot at request time and uses it on resume, surviving checkpoint/restore cycles. Both changes are well-structured with proper error handling, fallback paths for legacy checkpoints, and cleanup after completion. No concrete failure modes identified that meet the evidence threshold.
✗ Design Approach
I found one blocking design issue in the new function-approval snapshot flow. The restored snapshot arguments are passed straight into
AIFunctionArguments, so structured arguments can change shape after a checkpoint/restore cycle: the durable state layer round-trips them through JSON, and the repo already has a normalization step for this exact problem in the normal function-call path.
Flagged Issues
-
InvokeFunctionToolExecutorreplays restored approval arguments directly intoAIFunctionArgumentswithout callingNormalizePortableValues()(line 332). After a durable checkpoint/restore,Dictionary<string, object?>values deserialize asJsonElement, whereas the existing function-invocation path inWorkflowRunner.cs:346already normalizes these. Approved calls with nested arguments will silently invoke the function with different runtime types after restore.
Automated review by peibekwe's agents
…el/InvokeFunctionToolExecutor.cs Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Description
Bug fixes for declarative workflows
Contribution Checklist